<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="manifest" href="/app.webmanifest" />
  <link href="/libs/jsoneditor.min.css" rel="stylesheet" type="text/css" />
  <script src="/libs/jsoneditor.min.js"></script>
  <title>Slime Sim</title>
  <style>
    * {
      margin: 0;
      box-sizing: border-box;
    }
    
    .container {
      display: grid;
      grid-template-areas: 'main main main sidebar' 'main main main sidebar';
      gap: 10px;
      background-color: white;
      padding: 10px;
      width: 100vw;
      height: 100vh;
    }

    #canvas { 
      background-color: black;
    }

    .main {
      grid-area: main;
      overflow-x: auto;
      overflow-y: auto; 
    }

    .sidebar {
      grid-area: sidebar;
    }

    @media only screen and (max-width: 980px) {
      .main {
        grid-area: 1 / 1 / 1 / span 4;
        min-width: 100vw;
        min-height: 100vw;
      }

      .sidebar {
        grid-area: 2 / 1 / 2 / span 4;
      } 
      #record {
        display: none;
      }

    }

    @media only screen and (min-width: 980px) {
      .main {
        grid-area: main;
      }

      .sidebar {
        grid-area: sidebar;
      } 
      #record {
        display: inline;
      }
    
    }
  </style>
</head>
<body> 
  <h1>Slime Sim (WebGL)</h1>
  <div id="download"></div>
  <div class="container" >
    <div class="main">
      <canvas id="canvas" width="1000" height="1000" style="background-color: black"></canvas>
    </div>
    <div class="sidebar">
      <div id="control">
        <button id="btn-start" onclick="on_start();">Start</button>
        <button id="btn-pause" onclick="on_pause();" disabled>Pause</button>
        <button id="btn-stop" onclick="on_stop();" disabled>Stop</button>
        <input type="checkbox" id="record" name="record" value="false">Record</input>
      </div>
      <div id="jsoneditor"></div>
    </div>
  </div>
  <script type="text/x-vertex-shader" id="vs-update">#version 300 es
#ifndef RANDOM_SCALE
#ifdef RANDOM_HIGHER_RANGE
#define RANDOM_SCALE vec4(.1031, .1030, .0973, .1099)
#else
#define RANDOM_SCALE vec4(443.897, 441.423, .0973, .1099)
#endif
#endif
in vec2 i_pos;
in float i_dir;
in vec4 i_mask;
uniform sampler2D u_texture;
uniform vec4 sense_step;
uniform float sense_size;
uniform vec4 sense_angle;
uniform vec4 turn_step;
uniform vec4 step;
uniform float exclusive;
uniform float u_time;
out vec2 v_pos;
out float v_dir;
float TPI = 3.1415926 * 2.0;
uniform vec2 u_res;
out vec2 v_texcoord;
out vec4 v_mask;
vec4 sense_mask;
float random(in vec2 st) {
        vec3 p3  = fract(vec3(st.xyx) * RANDOM_SCALE.xyz);
        p3 += dot(p3, p3.yzx + 33.33);
        return fract((p3.x + p3.y) * p3.z);
}
float tv(vec2 pos, float x, float y) {
    float sx = pos.x + x;
    float sy = pos.y + y;
    if(sx < 0.0 || sx > u_res.x - 1.0 || sy < 0.0 || sy > u_res.y - 1.0) {
        return 0.0;
    } else {
        return dot(texture(u_texture, vec2(sx/u_res.x, sy/u_res.y)), sense_mask);
    }
}
float sense(float off, float ss) {
    float dir = i_dir + off;
    vec2 c = i_pos + vec2(cos(dir),sin(dir))* ss;
    float sum = tv(c,0.0, 0.0);
    sum += tv(c,-1.0, -1.0);
    sum += tv(c,0.0, -1.0);
    sum += tv(c,1.0, -1.0);
    sum += tv(c,1.0, 0.0);
    sum += tv(c,-1.0, 0.0);
    sum += tv(c,-1.0, 1.0);
    sum += tv(c,0.0, 1.0);
    sum += tv(c,1.0, 1.0);
    return sum;
 }
void main() {
    sense_mask =  i_mask + exclusive * (i_mask - vec4(1.0,1.0,1.0,1.0));
    float ss = dot(sense_step, i_mask);
    float sa = dot(sense_angle, i_mask);
    float fw = sense(0.0,ss);
    float lw = sense(sa,ss);
    float rw = sense(-sa,ss);
    float r = random(i_pos/u_res);
    v_dir = i_dir;
     if(fw < lw && fw < rw) {
        v_dir += (r - 0.5) * 2.0 * dot(turn_step,i_mask);
  } else if(lw < rw) {
        v_dir -=  r * dot(turn_step,i_mask);
  } else if(lw > rw) {
        v_dir +=  r * dot(turn_step,i_mask);
  }
    v_pos = i_pos + vec2(cos(v_dir), sin(v_dir)) * dot(step, i_mask);
    if(v_pos.x <= 0.0 || v_pos.x >= u_res.x - 1.0 || v_pos.y <= 0.0 || v_pos.y >= u_res.y - 1.0) {
        v_dir = random(v_pos/u_res) * TPI;
        v_pos = vec2(clamp(v_pos.x, 0.0, u_res.x - 1.0), clamp(v_pos.y, 0.0, u_res.y - 1.0));
    }
    v_texcoord = v_pos / u_res;
    gl_PointSize = 1.0;
    gl_Position = vec4(v_texcoord * 2.0 - 1.0, 0.0, 1.0);
    v_mask = i_mask;
}
</script>
  <script type="text/x-fragment-shader" id="fs-update">#version 300 es
precision highp float;
out vec4 v_color;
uniform sampler2D u_texture;
uniform vec4 trail_inc;
in vec2 v_texcoord;
in vec4 v_mask;

void main() {
    v_color = clamp(texture(u_texture, v_texcoord) + dot(v_mask, trail_inc) * v_mask, 0.0, 1.0);
}
</script>
  <script type="text/x-fragment-shader" id="fs-diffuse">#version 300 es
precision highp float;
out vec4 v_color;
uniform sampler2D u_texture;
uniform vec2 u_dsize;
uniform vec4 diff_rate;
uniform vec4 decay_rate;
in vec2 v_texcoord;

vec4 tv(int x, int y) {
    vec2 coord = v_texcoord + vec2(x,y)*u_dsize;
    return texture(u_texture, coord);
}

void main() {
    vec4 c = texture(u_texture, v_texcoord);
    vec4 a = c;
    a += tv(-1,-1);
    a += tv(0,-1);
    a += tv(1,-1);
    a += tv(-1,0);
    a += tv(1,0);
    a += tv(-1,1);
    a += tv(0,1);
    a += tv(1,1);
    a = diff_rate * (a / 9.0) +  (1.0 - diff_rate) * c;
    v_color = clamp(a - decay_rate, 0.0, 1.0);
}
</script>
  <script type="text/x-vertex-shader" id="vs-render">#version 300 es
in vec2 i_pos;
in vec2 i_texcoord;
out vec2 v_texcoord;
void main() {
    gl_Position = vec4(i_pos, 0.0, 1.0);
    v_texcoord = i_texcoord;
}
</script>
  <script type="text/x-fragment-shader" id="fs-render">#version 300 es
 precision highp float;
 out vec4 v_color;
 uniform sampler2D u_texture;
 uniform mat4 u_colors;
 in vec2 v_texcoord;
 void main() {
     vec4 c = texture(u_texture, v_texcoord);
     v_color = u_colors * c + vec4(0.0,0.0,0.0,1.0);
 }
</script>
  <script type="text/x-fragment-shader" id="fs-copy">#version 300 es
precision highp float;
out vec4 v_color;
uniform sampler2D u_texture;
in vec2 v_texcoord;
void main() {
    v_color = texture(u_texture, v_texcoord);
}
</script>
  <script>
    var FBYTES = new Float32Array().BYTES_PER_ELEMENT;

    function check_shader(shader) {
      var s = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (!s) {
        var err = gl.getShaderInfoLog(shader);
        gl.deleteShader(shader);
        throw "Fialed to compile\n" + err;
      }
    }

    function createProgram(gl, vsid, fsid, tfvs) {
      var vsource = document.getElementById(vsid).text;
      var fsource = document.getElementById(fsid).text;
      var vshader = gl.createShader(gl.VERTEX_SHADER);
      gl.shaderSource(vshader, vsource);
      gl.compileShader(vshader);
      check_shader(vshader);
      var fshader = gl.createShader(gl.FRAGMENT_SHADER);
      gl.shaderSource(fshader, fsource);
      gl.compileShader(fshader);
      check_shader(fshader);
      var prog = gl.createProgram();
      gl.attachShader(prog, vshader);
      gl.attachShader(prog, fshader);

      if (tfvs !== undefined) {
        gl.transformFeedbackVaryings(prog, tfvs, gl.INTERLEAVED_ATTRIBS);
      }

      gl.linkProgram(prog);
      gl.detachShader(prog, fshader);
      gl.deleteShader(fshader);
      gl.detachShader(prog, vshader);
      gl.deleteShader(vshader);
      var s = gl.getProgramParameter(prog, gl.LINK_STATUS);
      if (!s) {
        var err = gl.getProgramInfoLog(prog);
        gl.deleteProgram(prog);
        throw "Failed to link\n" + err;
      }
      return prog;
    }

    function loadBuffer(gl, data, type) {
      var buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, data, type);
      return buffer;
    }

    function setupVAOf(gl, prog, num, attr, norm, stride, offset) {
      var loc = gl.getAttribLocation(prog, attr);
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(
        loc,
        num,
        gl.FLOAT,
        norm,
        stride * FBYTES,
        offset * FBYTES
      );
    }
  </script>
  <script>
    const DW = Math.min(window.innerWidth, 1000);
    var canvas = document.getElementById("canvas");
    canvas.width = DW;
    canvas.height = DW
    var gl = canvas.getContext("webgl2");
    var W = DW, H = DW;
    var HW = W / 2.0,
      HH = H / 2.0;
    var MAX_SPECIES = 4;
    var TOTAL = 200000;
    var TPI = Math.PI * 2.0;
    var LAYOUT_INWARD = 0;
    var LAYOUT_OUTWARD = 1;

    function createUpdateVAO(gl, prog, init_data) {
      var vao = gl.createVertexArray();
      gl.bindVertexArray(vao);
      var pbuffer = loadBuffer(gl, init_data, gl.STREAM_DRAW);
      setupVAOf(gl, prog, 2, "i_pos", false, 3, 0);
      setupVAOf(gl, prog, 1, "i_dir", false, 3, 2);
      gl.bindVertexArray(null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      return { vao: vao, buffer: pbuffer };
    }

    function createTrailTexture(gl, arr) {
      var trail_tex = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, trail_tex);
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        W,
        H,
        0,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        arr
      );
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      return trail_tex;
    }

    function createInwardAgent(cx, cy, rmax) {
      var a = Math.random() * TPI;
      var r = Math.random() * rmax;
      var offset = i * 3;
      agents[offset] = cx + r * Math.cos(a);
      agents[offset + 1] = cy + r * Math.sin(a);
      agents[offset + 2] = a + Math.PI;
    }

    function inward_radius(cx, cy, r) {
      return (
        Math.min(
          (cx <= 0.5 ? cx : 1 - cx) * W,
          (cy <= 0.5 ? cy : 1 - cy) * H
        ) * r
      );
    }

    function inward_layout_func(conf) {
      var rmax = inward_radius(conf.cx, conf.cy, conf.radius);
      var x = conf.cx * W,
        y = conf.cy * H;
      return function (arr, offset) {
        var a = Math.random() * TPI;
        var r = Math.random() * rmax;
        arr[offset] = x + r * Math.cos(a);
        arr[offset + 1] = y + r * Math.sin(a);
        arr[offset + 2] = a + Math.PI;
      };
    }

    function outward_layout_func(conf) {
      var x = conf.cx * W,
        y = conf.cy * H;
      return function (arr, offset) {
        arr[offset] = x;
        arr[offset + 1] = y;
        arr[offset + 2] = Math.random() * TPI;
      };
    }

    function createUpdateProgam(gl, aconfs) {
      var prog = createProgram(gl, "vs-update", "fs-update", [
        "v_pos",
        "v_dir",
      ]);
      var agents = new Float32Array(TOTAL * 3);
      var mdata = new Float32Array(TOTAL * 4);
      var i = 0;
      for (var k = 0; k < aconfs.length; ++k) {
        var conf = aconfs[k];
        var num = conf.num;
        var m = conf.mask;
        var layout =
          conf.layout == LAYOUT_INWARD
            ? inward_layout_func(conf)
            : outward_layout_func(conf);
        for (var j = 0; j < num; ++i, ++j) {
          layout(agents, i * 3);
          //agents[offset] = HW;
          //agents[offset + 1] = HH;
          //agents[offset + 2] = a;
          //var m = masks[Math.floor(Math.random() * masks.length)];
          var offset = i * 4;
          mdata[offset] = m[0];
          mdata[offset + 1] = m[1];
          mdata[offset + 2] = m[2];
          mdata[offset + 3] = m[3];
        }
      }

      s = { program: prog };
      //read vao
      s.read = createUpdateVAO(gl, prog, agents);
      gl.bindVertexArray(s.read.vao);
      var mbuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, mbuffer);
      gl.bufferData(gl.ARRAY_BUFFER, mdata, gl.STATIC_DRAW);
      setupVAOf(gl, prog, 4, "i_mask", false, 0, 0);

      //write vao
      s.write = createUpdateVAO(gl, prog, agents);
      gl.bindVertexArray(s.write.vao);
      gl.bindBuffer(gl.ARRAY_BUFFER, mbuffer);
      setupVAOf(gl, prog, 4, "i_mask", false, 0, 0);

      gl.bindVertexArray(null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);

      s.read_texture = createTrailTexture(gl, null);
      s.write_texture = createTrailTexture(gl, null);
      s.frame_buffer = gl.createFramebuffer();
      return s;
    }

    function createRenderProgam(gl) {
      var prog = createProgram(gl, "vs-render", "fs-render");
      var vao = gl.createVertexArray();
      gl.bindVertexArray(vao);
      var pbuffer = loadBuffer(
        gl,
        new Float32Array([
          -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_pos", false, 0, 0);
      var tcbuffer = loadBuffer(
        gl,
        new Float32Array([
          0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_texcoord", false, 0, 0);
      gl.bindVertexArray(null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      return { program: prog, vao: vao };
    }

    function createCopyProgam(gl) {
      var prog = createProgram(gl, "vs-render", "fs-copy");
      var vao = gl.createVertexArray();
      gl.bindVertexArray(vao);
      var pbuffer = loadBuffer(
        gl,
        new Float32Array([
          -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_pos", false, 0, 0);
      var tcbuffer = loadBuffer(
        gl,
        new Float32Array([
          0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_texcoord", false, 0, 0);
      gl.bindVertexArray(null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      return { program: prog, vao: vao };
    }

    function createDiffuseProgam(gl) {
      var prog = createProgram(gl, "vs-render", "fs-diffuse");
      var vao = gl.createVertexArray();
      gl.bindVertexArray(vao);
      var pbuffer = loadBuffer(
        gl,
        new Float32Array([
          -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_pos", false, 0, 0);
      var tcbuffer = loadBuffer(
        gl,
        new Float32Array([
          0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
        ]),
        gl.STATIC_DRAW
      );
      setupVAOf(gl, prog, 2, "i_texcoord", false, 0, 0);
      gl.bindVertexArray(null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      return { program: prog, vao: vao };
    }

    function do_update(gl, pupdate, s) {
      gl.useProgram(pupdate.program);
      gl.bindVertexArray(pupdate.read.vao);
      gl.uniform2f(gl.getUniformLocation(pupdate.program, "u_res"), W, H);
      gl.uniform1i(gl.getUniformLocation(pupdate.program, "u_texture"), 0);
      gl.uniform4fv(
        gl.getUniformLocation(pupdate.program, "trail_inc"),
        s.trail_inc
      );
      gl.uniform1f(
        gl.getUniformLocation(pupdate.program, "u_time"),
        s.total_time
      );
      gl.uniform4fv(
        gl.getUniformLocation(pupdate.program, "sense_step"),
        s.sense_step
      );
      gl.uniform1f(
        gl.getUniformLocation(pupdate.program, "sense_size"),
        s.sense_size
      );
      gl.uniform4fv(
        gl.getUniformLocation(pupdate.program, "sense_angle"),
        s.sense_angle
      );
      gl.uniform4fv(
        gl.getUniformLocation(pupdate.program, "turn_step"),
        s.turn_speed
      );
      gl.uniform1f(
        gl.getUniformLocation(pupdate.program, "exclusive"),
        s.exclusive
      );
      gl.uniform4fv(gl.getUniformLocation(pupdate.program, "step"), s.step);
      gl.bindFramebuffer(gl.FRAMEBUFFER, pupdate.frame_buffer);
      gl.bindTexture(gl.TEXTURE_2D, pupdate.write_texture);
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        pupdate.write_texture,
        0
      );
      gl.bindTexture(gl.TEXTURE_2D, pupdate.read_texture);
      gl.bindBufferBase(
        gl.TRANSFORM_FEEDBACK_BUFFER,
        0,
        pupdate.write.buffer
      );
      gl.beginTransformFeedback(gl.POINTS);
      gl.drawArrays(gl.POINTS, 0, TOTAL);
      gl.endTransformFeedback();
      gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }

    function do_diffuse(gl, pdiffuse, s) {
      gl.useProgram(pdiffuse.program);
      gl.uniform1i(gl.getUniformLocation(pdiffuse.program, "u_texture"), 0);
      gl.uniform2f(
        gl.getUniformLocation(pdiffuse.program, "u_dsize"),
        1.0 / W,
        1.0 / H
      );
      gl.uniform4fv(
        gl.getUniformLocation(pdiffuse.program, "diff_rate"),
        s.diff_rate
      );
      gl.uniform4fv(
        gl.getUniformLocation(pdiffuse.program, "decay_rate"),
        s.decay_rate
      );
      gl.bindVertexArray(pdiffuse.vao);
      gl.bindFramebuffer(gl.FRAMEBUFFER, pdiffuse.frame_buffer);
      gl.bindTexture(gl.TEXTURE_2D, pdiffuse.write_texture);
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        pdiffuse.write_texture,
        0
      );
      gl.bindTexture(gl.TEXTURE_2D, pdiffuse.read_texture);
      gl.drawArrays(gl.TRIANGLES, 0, 6);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }

    function do_render(gl, prender, colors) {
      gl.useProgram(prender.program);
      gl.bindVertexArray(prender.vao);
      gl.bindTexture(gl.TEXTURE_2D, prender.read_texture);
      gl.uniform1i(gl.getUniformLocation(prender.program, "u_texture"), 0);
      gl.uniformMatrix4fv(
        gl.getUniformLocation(prender.program, "u_colors"),
        false,
        colors
      );
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    }

    function do_copy(gl, prender) {
      gl.useProgram(prender.program);
      gl.uniform1i(gl.getUniformLocation(prender.program, "u_texture"), 0);
      gl.bindVertexArray(prender.vao);
      gl.bindFramebuffer(gl.FRAMEBUFFER, prender.frame_buffer);
      gl.bindTexture(gl.TEXTURE_2D, prender.write_texture);
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        prender.write_texture,
        0
      );
      gl.bindTexture(gl.TEXTURE_2D, prender.read_texture);
      gl.drawArrays(gl.TRIANGLES, 0, 6);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }

    function create_programs() {
      var pupdate = createUpdateProgam(gl, state.agents);
      var pdiffuse = createDiffuseProgam(gl);
      pdiffuse.frame_buffer = pupdate.frame_buffer;
      pdiffuse.read_texture = pupdate.write_texture;
      pdiffuse.write_texture = pupdate.read_texture;
      var prender = createRenderProgam(gl);
      prender.read_texture = pdiffuse.write_texture;
      var pcopy = createCopyProgam(gl);
      pcopy.frame_buffer = pupdate.frame_buffer;
      pcopy.read_texture = pdiffuse.write_texture;
      pcopy.write_texture = pdiffuse.read_texture;
      programs = {
        pupdate: pupdate,
        pdiffuse: pdiffuse,
        prender: prender,
        pcopy: pcopy,
      };
    }

    function clear_programs() {
      console.log("clear");
      gl.deleteProgram(programs.pupdate.program);
      gl.deleteVertexArray(programs.pupdate.read.vao);
      gl.deleteVertexArray(programs.pupdate.write.vao);
      gl.deleteTexture(programs.pupdate.read_texture);
      gl.deleteTexture(programs.pupdate.write_texture);
      gl.deleteFramebuffer(programs.pupdate.frame_buffer);
      gl.deleteProgram(programs.pdiffuse.program);
      gl.deleteVertexArray(programs.pdiffuse.vao);
      gl.deleteProgram(programs.prender.program);
      gl.deleteVertexArray(programs.prender.vao);
      gl.deleteProgram(programs.pcopy.program);
      gl.deleteVertexArray(programs.pcopy.vao);
      programs = undefined;
    }
  </script>
  <style type="text/css">
    .jsoneditor-sort,
    .jsoneditor-search,
    .jsoneditor-transform,
    .jsoneditor-poweredBy {
      display: none;
    }


    .jsoneditor {
      border-color: #eee;
    }

    .jsoneditor-menu {
      background-color: darkgray;
      border-color: darkgray;
    }
  </style>
  <script>  
    let SETTINGS = {
      height: DW,
      width: DW,
      delta_time: 0.02,
      exclusive: true,
      S1: {
        color: "#ff0000ff",
        num: 100000,
        diff_rate: 3,
        decay_rate: 0.3,
        trail_inc: 8,
        sense_step: 35,
        sense_angle: 30,
        turn_speed: 360,
        step: 1,
        cx: 0.5,
        cy: 0.5,
        radius: 0.9,
        inward: true,
      },
      S2: {
        color: "#0000ffff",
        num: 100000,
        diff_rate: 6,
        decay_rate: 1.6,
        trail_inc: 8,
        sense_step: 15,
        sense_angle: 45,
        turn_speed: 1800,
        step: 1,
        cx: 0.5,
        cy: 0.5,
        radius: 0.9,
        inward: true,
      },
      S3: {
        color: "#00ff00ff",
        num: 0,
        diff_rate: 6,
        decay_rate: 1.6,
        trail_inc: 8,
        sense_step: 15,
        sense_angle: 45,
        turn_speed: 1800,
        step: 1,
        cx: 0.5,
        cy: 0.5,
        radius: 0.9,
        inward: true,
      },
      S4: {
        color: "#00ffffff",
        num: 0,
        diff_rate: 6,
        decay_rate: 1.6,
        trail_inc: 8,
        sense_step: 15,
        sense_angle: 45,
        turn_speed: 1800,
        step: 1,
        cx: 0.5,
        cy: 0.5,
        radius: 0.9,
        inward: true,
      },
    };
    let setting_valid = true;
    const options = {
      mode: "tree",
      modes: ["code", "tree"],
      //schema: schema,
      //schemaRefs: {},
      search: false,
      navigationBar: false,
      onCreateMenu: function (items, node) {
        return [];
      },
      onValidationError: function (errors) {
        setting_valid = errors.length == 0;
      },
    };
    const editor = new JSONEditor(
      document.getElementById("jsoneditor"),
      options
    );
    editor.set(SETTINGS);
    editor.expandAll();
  </script>
  <script>
    var mediaRecorder;
    const downloadVideo = async (blob) => {
      const div = document.getElementById("download");
      var url = URL.createObjectURL(blob);
      console.log(url);
      var a = document.createElement("a");
      a.href = url;
      a.download = "slime.webm";
      a.className = "button";
      a.innerText = "ffmpeg -i slime.webm -c:v copy slime.mp4";
      div.appendChild(a);
    };

    function stop_record() {
      if (mediaRecorder) {
        mediaRecorder.stop();
        console.log(mediaRecorder.state);
        console.log("recorder stopped");
        mediaRecorder = undefined;
      }
    }
    var chunks = [];
 
    function start_record() {
      try {
        var stream = canvas.captureStream(30);
        mediaRecorder = new MediaRecorder(stream, {
          mimeType: 'video/webm;codecs=h264',
          videoBitsPerSecond: 200*1024*1024
        })
        mediaRecorder.ondataavailable = function (e) {
          chunks.push(e.data);
        }
        mediaRecorder.onstop = function (e) {
          downloadVideo(new Blob(chunks, { 'type': 'video/webm;codecs=h264' }));
          console.log("Finished");
          chunks = [];
          mediaRecorder = undefined;
        }
        mediaRecorder.start();
        console.log(mediaRecorder.videoBitsPerSecond);
        console.log("recorder started");
      } catch (e) {
        mediaRecorder = undefined;
        console.error("Failed to start record", e);
      }
    }

  </script>
  <script>
    function set_attr(id, attr, v) {
      document.getElementById(id).setAttribute(attr, v);
    }

    function remove_attr(id, attr) {
      document.getElementById(id).removeAttribute(attr);
    }

    function set_text(id, t) {
      document.getElementById(id).innerText = t;
    }

    function RAD(a) {
      return (a * Math.PI) / 180.0;
    }

    var pause = false;
    var stop = false;
    var DLTTIME = 0.02;
    var overlapping = 3.0,
      overlapping_update = 0.5;
    var MASKS = [
      [1, 0, 0, 0],
      [0, 1, 0, 0],
      [0, 0, 1, 0],
      [0, 0, 0, 1],
    ];
    var state = {
      total_time: 0,
      diff_rate: new Float32Array(4),
      decay_rate: new Float32Array(4),
      trail_inc: new Float32Array(4),
      sense_size: 1,
      sense_step: new Float32Array(4),
      sense_angle: new Float32Array(4),
      turn_speed: new Float32Array(4),
      step: new Float32Array(4),
      colors: new Float32Array(16),
      agents: [],
      masks: [],
    };
    var programs;

    function color_value(v, offset) {
      const s = v.substr(offset, 2);
      return s.length > 0 ? parseInt(s, 16) / 255 : undefined;
    }

    function init_state(settings) {
      DLTTIME = settings.delta_time;
      W = settings.width;
      H = settings.height;
      HW = W / 2.0;
      HH = H / 2.0;
      canvas.width = W;
      canvas.height = H;
      state.total_time = 0;
      state.exclusive = settings.exclusive ? 1.0 : 0.0;
      var agents = [];
      TOTAL = 0;
      var DLT = 100000;
      for (var i = 0; i < MAX_SPECIES; ++i) {
        s = settings["S" + (i + 1)];
        if (s.num == 0) {
          continue;
        }
        state.diff_rate[i] = s.diff_rate * DLTTIME;
        state.decay_rate[i] = s.decay_rate * DLTTIME;
        state.trail_inc[i] = s.trail_inc * DLTTIME;
        state.sense_step[i] = s.sense_step;
        state.sense_angle[i] = RAD(s.sense_angle);
        state.turn_speed[i] = RAD(s.turn_speed) * DLTTIME;
        state.step[i] = s.step;
        const color = s.color;
        state.colors[i * 4] = color_value(color, 1);
        state.colors[i * 4 + 1] = color_value(color, 3);
        state.colors[i * 4 + 2] = color_value(color, 5);
        state.colors[i * 4 + 3] = color_value(color, 7) || 1;
        agents.push({
          num: s.num,
          mask: MASKS[i],
          layout: s.inward ? LAYOUT_INWARD : LAYOUT_OUTWARD,
          cx: s.cx,
          cy: 1 - s.cy,
          radius: s.radius,
        });
        TOTAL += s.num;
      }
      state.agents = agents;
      gl.viewport(0, 0, W, H);
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
      gl.activeTexture(gl.TEXTURE0 + 0);
    }

    function draw(ts) {
      if (!pause && !stop) {
        state.total_time = ts;
        var pupdate = programs.pupdate;
        do_update(gl, pupdate, state);
        do_diffuse(gl, programs.pdiffuse, state);
        do_render(gl, programs.prender, state.colors);
        do_copy(gl, programs.pcopy);
        var t = pupdate.write;
        pupdate.write = pupdate.read;
        pupdate.read = t;
        requestAnimationFrame(draw);
      }
      if (stop) {
        clear_programs();
      }
    }

    function on_start() {
      if (!setting_valid) {
        alert("Invalid Setting");
        return;
      }
      const settings = editor.get();
      init_state(settings);
      if (TOTAL == 0) {
        alert("the numble of agents is 0");
        return;
      }
      const record = document.querySelector('#record').checked;
      if (programs) {
        clear_programs();
      }
      create_programs();
      pause = false;
      stop = false;
      set_attr("btn-start", "disabled", true);
      remove_attr("btn-pause", "disabled");
      remove_attr("btn-stop", "disabled");
      set_text("btn-pause", "Pause");
      if (record) {
        start_record();
      }
      requestAnimationFrame(draw);
    }

    function on_pause() {
      pause = !pause;
      set_text("btn-pause", pause ? "Resume" : "Pause");
      if (!pause) {
        requestAnimationFrame(draw);
      }
    }

    function on_stop() {
      stop = true;
      stop_record();
      set_text("btn-pause", "Pause");
      remove_attr("btn-start", "disabled");
      set_attr("btn-pause", "disabled", true);
      set_attr("btn-stop", "disabled", true);
      if (pause) {
        clear_programs();
      }
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    }
  </script>
  <script>  
    if ('serviceWorker' in navigator) { 
      navigator.serviceWorker.ready.then(reg => { 
        console.log("Service Worker ready" + reg.scope); 
      });
      navigator.serviceWorker.register('/slimsimglsw.js')
        .then(function (reg) {
          console.log("Service Worker ready "  + reg.scope);
        })
        .catch(function (error) {
          console.log("Service Worker error ");
          console.error(error);
        });
    } else {
      console.warn('Service Worker not available');
    }
 
  </script>
</body>

</html>